6.11. Архитектурная практика
Введение: что такое архитектурная практика
Архитектурная практика — это совокупность методов, принципов, решений и компромиссов, применяемых при формировании структуры программной системы с учётом её функциональных требований, нефункциональных ограничений, жизненного цикла и контекста эксплуатации. В отличие от проектирования на уровне компонентов или модулей, архитектурная практика оперирует высокоуровневыми абстракциями, определяющими организацию системы в целом: её слои, границы ответственности, способы взаимодействия, стратегии масштабирования, восстановления и эволюции.
Архитектурная практика не сводится к выбору конкретного шаблона или фреймворка. Она включает в себя:
- осознанное проведение границ ответственности между частями системы;
- управление зависимостями и связностью (coupling);
- контроль за когезией (cohesion) на уровне доменов и сервисов;
- баланс между гибкостью и стабильностью;
- предвидение точек роста и потенциальных узких мест;
- выбор стратегии эволюции: развитие существующей архитектуры или её пересмотр.
Архитектор программного обеспечения — это не просто разработчик с расширенными полномочиями. Это специалист, который принимает решения, последствия которых проявятся через месяцы или годы. Его задача — обеспечить архитектурную целостность, то есть сохранение логической непротиворечивости системы в условиях неопределённости, изменения требований и расширения команды.
Далее мы рассмотрим ключевые аспекты архитектурной практики, начиная с классических подходов и постепенно переходя к современным стратегиям декомпозиции и управления сложностью.
1. Компоненты монолита. Двухзвенная и трёхзвенная архитектура
Монолитная архитектура — исторически первая и наиболее интуитивно понятная форма организации приложений. В такой системе всё программное обеспечение разрабатывается, компилируется, развёртывается и масштабируется как единый артефакт. Однако важно понимать: термин «монолит» относится к развёртыванию и жизненному циклу, а не к внутренней структуре кода. Существуют хорошо структурированные монолиты — и разваливающиеся на части модульные системы, где отсутствует архитектурная дисциплина.
Классический монолит состоит из следующих логических компонентов:
- Пользовательский интерфейс (UI) — слой представления, реализующий взаимодействие с конечным пользователем. Может быть веб-интерфейсом (HTML/CSS/JS), десктопным приложением, мобильным клиентом.
- Бизнес-логика (application layer) — ядро системы, где реализуются правила предметной области, оркестрация операций, транзакционные гарантии.
- Доступ к данным (data access layer) — компоненты, отвечающие за взаимодействие с постоянным хранилищем: ORM, репозитории, DAO, клиенты баз данных.
Несмотря на единую точку развёртывания, даже в монолите возможна внутренняя модульность — например, через пространства имён, отдельные библиотеки, чёткие правила инверсии зависимостей (Dependency Inversion Principle). Однако границы между модулями в монолите остаются логическими, а не физическими, и их нарушение приводит к накоплению технического долга.
Двухзвенная архитектура
Двухзвенная (2-tier) архитектура — упрощённая модель, где клиентское приложение напрямую взаимодействует с СУБД. Например: десктопное приложение, подключающееся к PostgreSQL по сети. В такой схеме бизнес-логика либо отсутствует, либо частично размещается на клиенте, либо — в триггерах и хранимых процедурах. Это подход типичен для небольших информационных систем конца 1990‑х — начала 2000‑х.
Преимущества:
- Минимальная инфраструктура;
- Простота развёртывания (один клиент, один сервер БД);
- Низкая сетевая латентность между клиентом и БД.
Недостатки:
- Отсутствие централизованной бизнес-логики, что затрудняет поддержку согласованности при множестве клиентов;
- Высокая связность клиентского кода с моделью данных;
- Сложности с безопасностью: клиент оперирует учётной записью с прямыми правами на БД;
- Плохая масштабируемость: при росте числа клиентов нагрузка ложится на СУБД без возможности горизонтального масштабирования логики.
Трёхзвенная архитектура
Трёхзвенная (3-tier) архитектура — естественное развитие двухзвенной. Здесь явно выделяется сервер приложений как отдельный слой между клиентом и базой данных. Клиент общается только с сервером приложений, тот, в свою очередь, взаимодействует с БД через строго контролируемые интерфейсы.
Структурно:
- Presentation tier — клиент (тонкий или толстый, об этом ниже);
- Application tier — сервер приложений: веб-сервер, API-шлюз, бизнес-сервисы;
- Data tier — СУБД, кэши, файловые хранилища.
Трёхзвенная архитектура стала доминирующей в эпоху веб-приложений и остаётся актуальной для большинства enterprise-систем. Она позволяет:
- Централизовать бизнес-правила;
- Внедрять аутентификацию, авторизацию, аудит, логирование на уровне сервера;
- Гибко масштабировать логику независимо от хранилища;
- Поддерживать несколько клиентов (веб, мобильный, интеграции) через единый контракт.
Однако даже трёхзвенная система может быть монолитной — если все три слоя поставляются в одном исполняемом артефакте и масштабируются синхронно.
2. Плюсы и минусы монолитной архитектуры
Монолит остаётся обоснованным выбором при определённых условиях.
Преимущества монолита
- Простота разработки и отладки на ранних этапах. Единая кодовая база, единая IDE-сессия, возможность пошаговой отладки от UI до БД — всё это ускоряет итерации в стартовой фазе проекта.
- Единая транзакционная граница. Любая операция, затрагивающая несколько модулей, может быть обёрнута в одну ACID-транзакцию. Это упрощает обеспечение консистентности без сложных механизмов компенсации.
- Минимальная сетевая сложность. Отсутствие межсервисного взаимодействия означает отсутствие задержек, десериализации, обработки таймаутов, idempotency, circuit breakers.
- Упрощённый CI/CD. Один pipeline, один артефакт, одна операция деплоя — меньше точек отказа в процессе поставки.
- Низкие накладные расходы на инфраструктуру. Один процесс, один контейнер, один экземпляр БД — проще администрировать, дешевле в эксплуатации.
Недостатки монолита
- Связность (coupling) растёт нелинейно. По мере роста команды и функционала границы между модулями стираются: появляются циклические зависимости, прямые вызовы между «слоями», дублирование логики. Архитектура деградирует в «большой комок грязи» (Big Ball of Mud).
- Масштабирование «всё или ничего». Нельзя масштабировать только нагруженный компонент — придётся реплицировать весь монолит, даже если 90 % его функционала простаивает.
- Ограниченная технологическая гибкость. Смена языка, фреймворка или СУБД затрагивает всю систему. Эксперименты с новыми технологиями в отдельных модулях невозможны.
- Долгая сборка и деплой. При размере кодовой базы в сотни тысяч строк время компиляции, тестирования и развёртывания может измеряться десятками минут, что замедляет цикл обратной связи.
- Риск единой точки отказа. Падение одного компонента (например, из-за memory leak в модуле отчётов) выводит из строя всю систему.
- Сложность вовлечения новых разработчиков. Требуется понимание всей системы, даже если человек работает над узкой функцией.
Таким образом, монолит наиболее эффективен в условиях:
- небольшой команды (до 5–7 человек);
- узкого и чётко определённого домена;
- отсутствия требований к частому масштабированию или изоляции компонентов.
3. Понятие software architect и архитектурной ответственности
Архитектор программного обеспечения — это лицо, принимающее решения, касающиеся структуры системы, и несущее ответственность за их последствия. Его роль заключается в управлении сложностью через абстракцию и ограничение вариативности.
Ключевые обязанности архитектора:
- Формулировка архитектурного видения — краткое, понятное описание того, как система должна быть устроена, чтобы соответствовать стратегическим целям.
- Определение ключевых нефункциональных требований (NFR): latency, throughput, availability, security, evolvability — и их количественная спецификация (SLI/SLO).
- Выбор архитектурного стиля и паттернов, соответствующих контексту.
- Введение и поддержка архитектурных ограничений (constraints): запреты на прямые вызовы между модулями, требования к интерфейсам, правила именования, подходы к обработке ошибок.
- Архитектурные ревью — систематическая проверка соответствия решений видению.
- Обучение команды: объяснение почему принято то или иное решение, а не только что делать.
Архитектор не обязан писать код ежедневно, но должен быть способен сделать это — иначе его решения рискуют стать оторванными от реальности. Идеально, когда архитектор участвует в написании критически важных компонентов (например, ядра доменной логики или шлюза агрегации), чтобы сохранять «тактильную» связь с системой.
4. Архитектурные паттерны (не путать с GoF)
Архитектурные паттерны — это проверенные стратегии организации всей системы или её крупных частей. Они находятся на более высоком уровне абстракции по сравнению с паттернами проектирования (например, наблюдатель, фабрика), и решают задачи масштаба: масштабируемость, отказоустойчивость, поддерживаемость, изолированность изменений.
Некоторые ключевые архитектурные паттерны:
- Слоистая архитектура (Layered Architecture) — классическая трёх- или четырёхслойная структура (presentation, business, persistence, database). Подходит для приложений с чётким разделением ответственности и умеренным темпом изменений.
- Шина событий (Event Bus / Event-Driven Architecture) — компоненты взаимодействуют через асинхронные события, публикуемые в шину (Kafka, RabbitMQ, NATS). Поддерживает слабую связность, но требует дисциплины в моделировании событий и обработке ошибок.
- Ориентированная на команды и запросы (CQRS) — разделение операций записи и чтения на разные модели и, зачастую, разные физические компоненты.
- Микросервисная архитектура — декомпозиция по границам домена с физическим разделением развёртывания и данных.
- Serverless / FaaS — делегирование управления инфраструктурой облачному провайдеру, фокус на функциях как единицах развёртывания.
- Service Mesh — вынос логики коммуникации (retry, circuit breaking, mTLS) во внешний слой (sidecar-proxy), освобождая бизнес-код от инфраструктурной сложности.
Выбор архитектурного паттерна — это стратегическое решение. Например, переход на микросервисы без изменений в процессы команды (CI/CD, мониторинг, эксплуатация) часто ведёт к распределённому монолиту — системе, которая объединяет недостатки обоих миров.
5. TL;DR как инструмент архитектурного мышления
TL;DR («Too Long; Didn’t Read») в профессиональной среде — дисциплина архитектурного синтеза. Архитектор обязан уметь выразить суть решения в 2–4 предложения, доступных как техническому руководителю, так и нетехническому заказчику. Это требует:
- чёткого разделения проблемы и решения;
- отказа от жаргона без пояснения;
- фокуса на последствиях, а не на механизмах.
Пример корректного TL;DR для перехода на CQRS:
«Разделение операций записи и чтения позволяет оптимизировать каждую сторону под свои нагрузки: запись — через транзакции и валидацию в домене, чтение — через денормализованные проекции. Это повышает масштабируемость интерфейсов и устойчивость к пиковым нагрузкам, но вводит eventual consistency и требует механизмов синхронизации».
Некорректный вариант:
«Будем использовать CQRS с Event Sourcing и Kafka, это модно и правильно» — отсутствует проблема, последствия и контекст.
TL;DR — это стартовая точка для дискуссии. Он должен вызывать конкретные вопросы: Какой уровень задержки допустим при чтении? Как обрабатывать конфликты при одновременной записи? Как обеспечить аудит? Если TL;DR не порождает уточняющих вопросов — он либо слишком общий, либо скрывает сложность.
6. Шардирование: управление масштабом данных
Шардирование — стратегия горизонтального разделения единого логического набора данных на несколько физических сегментов (шардов), каждый из которых содержит подмножество записей. Цель — преодолеть ограничения одной инстанции СУБД по объёму данных, числу операций в секунду или пропускной способности дисковой подсистемы.
Ключевые принципы
- Шардирующий ключ — атрибут (или комбинация), по которому происходит распределение записей. Примеры:
user_id,tenant_id,region_code. Ключ должен обеспечивать равномерное распределение нагрузки и локальность данных — запросы по одному пользователю/клиенту должны попадать в один шард. - Прозрачность для приложения — идеально, если логика шардирования инкапсулирована в слое доступа к данным (например, через библиотеку вроде Hibernate Shards или Vitess). Однако на практике часто приходится вносить изменения в бизнес-логику: например, запрещать JOIN’ы между сущностями из разных шардов.
- Ребалансировка — процесс перераспределения данных при изменении числа шардов. Вручную это дорого и рискованно; автоматизированные решения (Vitess, Citus, YugabyteDB) включают координационные узлы и фоновые задачи репликации.
Типы шардирования
- Диапазонное — шарды выделяются по диапазонам значений ключа (
0–999,1000–1999). Просто в реализации, но подвержено дисбалансу при неравномерном распределении ключей (например, если большинство пользователей — из одного региона). - Хеширование — ключ хешируется (например, CRC32), и по остатку деления определяется шард. Обеспечивает равномерное распределение, но усложняет диапазонные запросы (
WHERE created_at BETWEEN …). - Географическое — шарды привязаны к регионам для минимизации latency и соблюдения требований локализации данных (например, GDPR). Требует явного управления репликацией между регионами.
Ограничения и риски
- Глобальные операции становятся сложными. Агрегация (
COUNT,SUM), сортировка, JOIN’ы — требуют координации между шардами, что снижает производительность и повышает latency. - Усложнение администрирования. Резервное копирование, восстановление, миграции схемы — должны выполняться для каждого шарда отдельно или с координацией.
- Жёсткая привязка к ключу. Изменение шардирующего ключа — операция уровня переписывания системы.
Шардирование оправдано, когда:
- объём данных превышает 1–2 ТБ на инстанс;
- write/read throughput близок к пределу одного узла;
- требования к latency не позволяют использовать вертикальное масштабирование.
В остальных случаях предпочтительны более простые стратегии: партиционирование внутри СУБД, read replicas, кэширование.
7. MV* паттерны: разделение ответственности на уровне представления
MVC, MVP, MVVM — это архитектурные паттерны для клиентских приложений, направленные на отделение логики от отображения. Несмотря на широкую известность, их часто смешивают или применяют механически, не понимая различий в потоках данных.
MVC (Model-View-Controller)
- Model — состояние и бизнес-логика, не зависящие от UI.
- View — пассивный элемент отображения, который оповещается об изменениях модели (часто через observer).
- Controller — обрабатывает входные события (клик, ввод), изменяет модель, инициирует обновление View.
Особенность: View имеет прямой доступ к Model для чтения (но не для записи). Контроллер — thin, так как основная логика в модели. Типично для серверных веб-фреймворков (Ruby on Rails, Spring MVC), где View — это шаблон, отрендеренный на сервере.
MVP (Model-View-Presenter)
- Presenter берёт на себя всю логику взаимодействия: получает события от View, читает/пишет в Model, явно обновляет View через интерфейс (например,
IView.UpdateUserName()). - View полностью пассивна — не имеет доступа к Model, не знает о бизнес-правилах. Это позволяет легко писать unit-тесты для Presenter’а.
Распространён в desktop- и mobile-разработке (Android до Jetpack Compose, WinForms), где важна тестируемость UI-логики.
MVVM (Model-View-ViewModel)
- ViewModel — представление состояния для View. Он предоставляет observable-свойства (например,
userName: string,isLoading: boolean). - View подписывается на изменения ViewModel (через data binding), автоматически обновляя интерфейс.
Ключевое отличие: поток данных — реактивный, однонаправленный. MVVM наиболее эффективен в средах с мощными binding-фреймворками (WPF, SwiftUI, Angular, Vue, React с MobX/Zustand).
Выбор паттерна
Решение зависит от:
- платформы (сервер vs клиент, наличие binding);
- требований к тестируемости;
- командных компетенций.
Например, в SPA на React чистый MVC не реализуем — компонентная модель ближе к MVVM (state → render), но с императивными элементами (useEffect как Presenter). Важно соблюдение принципа: изменение UI не должно требовать изменения бизнес-логики, и наоборот.
8. CQRS: разделение команд и запросов
Command Query Responsibility Segregation — принцип, утверждающий: операции, изменяющие состояние (команды), следует строго отделять от операций, только читающих состояние (запросы). В классической реализации это означает:
- две разные модели данных:
- Write model — нормализованная, транзакционная, ориентированная на целостность;
- Read model — денормализованная, оптимизированная под конкретные UI-сценарии, часто хранящаяся в NoSQL или колоночной БД.
- два разных API:
POST /orders— команда, создающая заказ;GET /order-summary/{id}— запрос, возвращающий агрегированные данные для карточки заказа.
Почему это работает
- Оптимизация под нагрузку. Write-сторона масштабируется по числу транзакций (ACID), read-сторона — по количеству запросов (возможно, с кэшированием и CDN).
- Эволюционная гибкость. Можно менять read-модель без пересборки write-логики — например, добавить новый отчёт, просто создав новую проекцию.
- Изоляция сложности. Валидация, агрегация, инварианты — остаются в write-модели, не засоряя read-сторону.
Проблемы и компромиссы
- Eventual consistency. Read-модель обновляется асинхронно — между записью и отображением может быть задержка (мс–с). Требуется продумать UX: показывать «сохранено», но «данные обновятся через несколько секунд».
- Сложность синхронизации. Проекции могут «отставать»; нужны механизмы повторной обработки событий, идемпотентности, отката.
- Удвоение кода. Две модели, две схемы, два набора тестов.
CQRS не требует Event Sourcing (хотя часто используется с ним). Можно применять CQRS поверх реляционной БД: write — в PostgreSQL, read — в проекции на Redis или Elasticsearch, обновляемые триггерами или фоновыми job’ами.
CQRS оправдан, когда:
- read/write нагрузки сильно различаются по объёму или структуре;
- требуется высокая масштабируемость интерфейсов;
- доменная модель сложна, и попытки «одной модели на всё» приводят к компромиссам.
Для простых CRUD-приложений CQRS — избыточность, создающая технический долг.
9. Декомпозиция монолита: модульность и микросервисы
Декомпозиция — средство снижения сложности. Она возможна в двух плоскостях: логической (модули в едином артефакте) и физической (независимые процессы, развёртывания, данные).
Декомпозиция на модули
Модуль — автономная единица кода с чётким контрактом, минимальными внешними зависимостями и высокой внутренней когезией. В .NET — отдельная сборка (Project.dll), в Java — JAR-модуль, в Python — пакет с явными __init__.py и py.typed.
Принципы модульной декомпозиции:
- Инверсия зависимостей. Высокоуровневые модули не зависят от низкоуровневых — оба зависят от абстракций (интерфейсов, DTO).
- Замкнутость по модификации (OCP). Изменение одного модуля не требует изменения других — только при расширении контракта.
- Явное управление видимостью. Публичные API модуля — минимальны; внутренние классы скрыты (
internalв C#, package-private в Java). - Автономная сборка и тестирование. Модуль можно собрать и прогнать unit-тесты без остальной системы.
Инструменты поддержки:
- в .NET —
InternalsVisibleTo,PrivateAssets, анализ зависимостей черезdotnet-dependency-analyzer; - в Java — JPMS (Java Platform Module System),
module-info.java; - в Python —
mypyс strict mode,pydepsдля визуализации связей.
Модульный монолит — переходная, но устойчивая архитектура. Он сохраняет преимущества монолита (единая транзакция, простой деплой), но устраняет основную его слабость — неуправляемую связность.
Декомпозиция на микросервисы
Микросервис — независимо развёртываемая, слабосвязанная единица, ответственная за конкретную бизнес-возможность в рамках ограниченного контекста (bounded context).
Критерии «настоящего» микросервиса:
- Собственная граница данных (отдельная БД или схема, запрет прямого доступа извне);
- Собственный жизненный цикл (CI/CD, версионирование, откат);
- Собственный мониторинг и логирование (tracing ID, метрики по сервису);
- Контрактный интерфейс (API, события) — единственный способ взаимодействия.
Декомпозиция на микросервисы должна начинаться с анализа домена (см. DDD ниже). Частая ошибка — разрезание по техническим признакам («сервис пользователей», «сервис отчётов»), что приводит к тесной связности и «распределённой транзакции» через REST.
10. Управление данными в микросервисной архитектуре
В микросервисной архитектуре каждый сервис владеет своими данными и имеет исключительное право на их изменение. Это принцип data ownership, лежащий в основе слабой связности. Прямой доступ к БД другого сервиса — архитектурное нарушение, ведущее к скрытым зависимостям и невозможности независимой эволюции.
Проблема распределённых транзакций
Классическая ACID-транзакция, охватывающая несколько сервисов, невозможна без введения централизованного координатора (2PC/XA), что противоречит целям микросервисов: отказоустойчивости и автономности. Вместо этого применяются компенсирующие стратегии:
-
Saga — последовательность локальных транзакций, каждая из которых сопровождается компенсирующей операцией на случай отката.
Пример:ReserveInventory→ в случае ошибкиCancelReservation;ChargePayment→ в случае ошибкиRefund;ShipOrder→ в случае ошибкиCancelShipment.
Saga может быть хореографией (каждый сервис реагирует на события) или оркестрацией (центральный orchestrator управляет потоком).
-
Outbox-паттерн — обеспечение атомарности «изменение данных + публикация события». При обновлении записи в БД в ту же транзакцию добавляется запись во внутреннюю таблицу
outbox. Фоновый процесс забирает события изoutboxи отправляет в шину (Kafka, RabbitMQ). Это исключает потерю событий при сбое после коммита БД, но до отправки. -
Event Sourcing — хранение последовательности событий, его породивших. Состояние восстанавливается проигрыванием лога. Позволяет легко строить проекции, аудит, откаты — но требует дисциплины в моделировании событий и управления эволюцией схемы.
Выбор хранилища
Микросервисы могут использовать разные типы БД — в зависимости от характера данных:
- Реляционные (PostgreSQL, MySQL) — для транзакционных операций с сильной целостностью;
- Документные (MongoDB, Couchbase) — для гибких, иерархических сущностей с редкими JOIN’ами;
- Ключ-значение (Redis, DynamoDB) — для кэширования, сессий, временных данных;
- Графовые (Neo4j) — для отношений, где важны связи (социальные графы, маршрутизация);
- Временные ряды (InfluxDB, TimescaleDB) — для метрик, логов, IoT-данных.
Важно: использование нескольких типов БД (polyglot persistence) оправдано только при явной необходимости. Добавление нового типа — это долгосрочное обязательство по обучению команды, мониторингу и резервному копированию.
Гарантии консистентности
В распределённых системах приходится выбирать между:
- Сильной консистентностью (linearizability) — дорого, плохо масштабируется;
- Eventual consistency — данные сойдутся со временем, дешевле и устойчивее.
Архитектор должен явно определить, для каких операций допустима eventual consistency, а где нужна транзакционная изоляция. Например:
- Баланс счёта — strong consistency (через Saga с блокировками);
- Отображение рекомендаций — eventual consistency (данные обновляются раз в минуту).
11. Толстый и тонкий клиент: распределение логики
Термины «толстый» и «тонкий» клиент относятся к тому, где выполняется оркестрация бизнес-логики — на стороне клиента или сервера.
Тонкий клиент (thin client)
- Выполняет только отображение и сбор ввода.
- Вся логика — на сервере: валидация, агрегация, преобразование данных.
- Коммуникация — через высокоуровневые API (например,
GET /dashboard-data, возвращающий готовый DTO для UI). - Преимущества:
— единая точка контроля логики;
— простота поддержки нескольких клиентов (веб, мобильный, интеграции);
— безопасность: клиент не видит внутренние структуры данных. - Недостатки:
— высокая нагрузка на сервер при сложных UI;
— latency при множестве микровызовов («chatty API»);
— сложность кэширования на клиенте.
Толстый клиент (thick client)
- Содержит значительную часть логики: маршрутизацию, кэширование, предварительную валидацию, локальное состояние.
- Сервер предоставляет низкоуровневые интерфейсы (например,
GET /users?id=123,GET /orders?userId=123). - Клиент сам оркестрирует вызовы, агрегирует данные, управляет состоянием (например, через Redux или Zustand).
- Преимущества:
— отзывчивый интерфейс (меньше round-trip’ов);
— автономность (offline-режим);
— гибкость UI без изменения сервера. - Недостатки:
— дублирование логики между клиентами;
— сложность тестирования и поддержки;
— риск утечки бизнес-логики в клиентский код.
Современный гибрид: BFF (Backend for Frontend)
Наиболее сбалансированный подход — выделение специализированных серверных шлюзов под каждый тип клиента:
web-bff— агрегирует данные для веб-интерфейса, кэширует, адаптирует под роутинг SPA;mobile-bff— сжимает данные, оптимизирует под медленные сети, управляет версионированием API;integration-bff— предоставляет контракты для внешних партнёров.
BFF — это архитектурная граница, защищающая внутренние сервисы от прямого воздействия клиентов. Он принадлежит команде фронтенда и развивается в её же цикле.
12. Коммуникация микросервисов
Выбор протокола и стиля взаимодействия — один из самых критичных архитектурных решений. Ошибки здесь приводят к неустойчивым, медленным, неотлаживаемым системам.
Синхронная коммуникация
-
REST/HTTP — де-факто стандарт. Прост в отладке, поддерживается повсеместно.
Риски:
— каскадные отказы при таймаутах;
— сложность обеспечения idempotency (повторный вызов не должен создавать дубль);
— неявные зависимости через цепочки вызовов (A → B → C → D). -
gRPC — бинарный, контрактный, с поддержкой streaming и строгой типизацией через proto-файлы.
Преимущества:
— высокая производительность;
— генерация клиентов и серверов;
— встроенный deadline propagation.
Недостатки:
— сложность отладки без инструментов (нужен BloomRPC или подобное);
— слабая поддержка в браузере (требуется gRPC-Web + proxy).
Асинхронная коммуникация
-
Событийная (event-driven) — сервис публикует событие (
OrderCreated), другие — подписываются.
Ключевые требования:
— идемпотентность обработчиков (событие может прийти дважды);
— управление порядком (Kafka гарантирует порядок в партиции, но не глобально);
— отслеживание состояния обработки (checkpointing, dead-letter queues). -
Командная (command-driven) — явная отправка команды (
ApproveOrder) конкретному получателю. Часто реализуется поверх очередей (RabbitMQ с routing keys).
Гибридные схемы
На практике используется комбинация:
- Запросы — синхронно (REST/gRPC), с жёсткими SLA по latency;
- Изменения состояния — асинхронно (события), с eventual consistency.
Не каждая операция требует ответа. Например, отправка уведомления — это фоновое событие.
13. Построение пользовательского интерфейса в распределённых системах
UI — это архитектурный элемент, подверженный тем же требованиям к масштабируемости и эволюции.
Проблемы интеграции UI-компонентов
При декомпозиции по доменам возникает вопрос: как собрать единую страницу из данных нескольких сервисов?
-
Микрофронтенды (Micro Frontends) — подход, при котором независимые команды поставляют фрагменты UI (например, через Web Components, Module Federation в Webpack 5 или single-spa).
Преимущества:
— независимое развёртывание частей интерфейса;
— выбор технологий на уровне фичи (React для аналитики, Vue для админки).
Риски:
— дублирование зависимостей (множество копий React);
— сложность обеспечения единого UX/UI-kit’а;
— проблемы с общей аутентификацией и состоянием. -
BFF для агрегации — более контролируемый подход. BFF вызывает несколько сервисов, собирает DTO, применяет преобразования (например, маппинг
UserDTO + OrderDTO → DashboardViewModel) и отдаёт клиенту структуру, идеально подходящую под текущий рендер.
Единый UX без единой кодовой базы
Чтобы избежать «лоскутного одеяла», вводятся:
- Design System — библиотека компонентов, токенов, гайдлайнов;
- Контракты интерфейсов — API, UI-контракты: форматы метаданных, правила локализации, семантика уведомлений;
- Сквозная идентификация — единый tracing ID, передаваемый от UI до БД.
14. Повышение отказоустойчивости
Отказоустойчивость — системный подход к управлению сбоями. Архитектор должен предполагать, что всё будет ломаться — и проектировать соответственно.
Основные принципы
- Изоляция (bulkheading) — ограничение распространения сбоев. Пример: пулы соединений к БД выделяются по сервисам; падение одного сервиса не исчерпывает все соединения.
- Таймауты — все внешние вызовы должны иметь явный deadline. Бесконечное ожидание — худший режим отказа.
- Circuit Breaker — при превышении порога ошибок вызовы временно блокируются, возвращается fallback-ответ. Библиотеки: Polly (.NET), Resilience4j (Java), Istio (на уровне service mesh).
- Грациозная деградация (graceful degradation) — при частичном отказе система продолжает работать в ограниченном режиме.
Пример:
— недоступен сервис рекомендаций → показываем популярные товары из кэша;
— недоступна аналитика → логируем в локальный буфер, отправляем позже.
Fallback-стратегии
- Кэш как fallback — при ошибке чтения из БД — вернуть устаревшие данные из Redis.
- Статический ответ — «Сейчас высокая нагрузка, повторите через 5 минут».
- Очередь с отложенной обработкой — запись в локальный store + фоновая синхронизация.
fallback не должен быть «заглушкой». Он должен быть тестируемым и мониторинг-дружелюбным (метрика fallback_triggered{service="orders"}).
15. Проведение архитектурных границ
Архитектурная граница — это контролируемая точка взаимодействия между компонентами, в которой явно определены:
- контракт (входные/выходные данные, семантика ошибок);
- допустимые зависимости (что может вызывать что);
- стратегия изменения (как вносить breaking changes без катастрофы).
Границы проводят по следующим критериям:
По скорости изменения
Компоненты, меняющиеся с разной частотой, должны быть разделены. Пример:
- ядро доменной логики — редко (раз в квартал);
- интеграции с внешними системами — часто (еженедельно);
- UI-слои — постоянно (ежедневно).
Если всё собрано в один модуль — любая правка интеграции требует полного регрессионного тестирования ядра.
По уровню критичности
Компоненты с разными требованиями к надёжности, безопасности и аудиту лучше изолировать:
- обработка платежей — строгий аудит, двухфакторная валидация, отдельная БД;
- публичный каталог товаров — кэшируемый, read-only, без персональных данных.
Смешение приводит либо к избыточным ограничениям для некритичных частей, либо к риску в критичных.
По принадлежности к команде
Граница должна совпадать с организационной (Conway’s Law). Если две команды работают над одним модулем — возникнет координационный оверхед, merge-конфликты, компромиссы в архитектуре. Разделение по границам ответственности снижает transaction cost взаимодействия.
Инструменты фиксации границ
- Контрактные тесты (consumer-driven contracts) — Pact, Spring Cloud Contract. Каждый потребитель фиксирует, какие поля и поведения он ожидает; провайдер проверяет совместимость до деплоя.
- Статический анализ зависимостей — ArchUnit (Java), NDepend (.NET), Deptrac (PHP). Запрещает вызовы между пакетами/слоями на уровне CI.
- Чёткие правила именования — например,
*.Core,*.Integration,*.UI— сразу видно, к какому слою относится компонент.
Граница — проходной пункт с досмотром. Чем строже контроль на границе, тем свободнее внутренняя реализация.
16. Повышение уровня абстракции интерфейсов
Интерфейс — это публичный контракт между системами. Его качество определяет долгосрочную жизнеспособность архитектуры.
Принципы проектирования контрактов
-
Семантическая насыщенность — имя операции должно отражать намерение, а не механизм.
Плохо:POST /api/v1/update— что обновляется? как?
Хорошо:PATCH /orders/{id}/cancel— ясное намерение, идемпотентно. -
Стабильность через версионирование — в заголовках (
Accept: application/vnd.myapp.order+json;version=2). Позволяет гибко управлять совместимостью. -
Гипермедиа как двигатель состояния (HATEOAS) — ответ содержит данные и возможные следующие действия:
{
"id": "ORD-123",
"status": "confirmed",
"_links": {
"cancel": { "href": "/orders/ORD-123/cancel", "method": "POST" },
"ship": { "href": "/orders/ORD-123/ship", "method": "POST" }
}
}Это делает клиент устойчивым к изменениям в workflow: если
shipнедоступен, ссылка просто отсутствует. -
Явная обработка ошибок —
409 Conflictс телом:{
"code": "INSUFFICIENT_STOCK",
"message": "Товара недостаточно на складе",
"details": { "available": 5, "requested": 10 }
}Это позволяет клиенту принимать осмысленные решения (предложить альтернативу, запросить подтверждение).
Отказ от «CRUD-мышления»
Многие API проектируются как тонкая обёртка над таблицами: GET /users, POST /users, PUT /users/123. Это приводит к:
- утечке внутренней структуры (например,
password_hashв ответе); - невозможности выразить бизнес-операции (
transferFunds,mergeAccounts); - жёсткой связности клиентов с моделью данных.
Вместо этого — операционно-ориентированные интерфейсы, отражающие действия предметной области.
17. Выделение доменов: Domain-Driven Design как архитектурный инструмент
DDD — набор техник для работы со сложностью в предметной области. Его ценность для архитектора — в структурировании мышления.
Ограниченный контекст (Bounded Context)
Это граница смысла: внутри контекста термины имеют однозначное определение, между контекстами — могут различаться.
Пример:
- В контексте
Billing:Customer— плательщик с реквизитами; - В контексте
Support:Customer— человек с историей обращений.
Смешение контекстов ведёт к «размытой» модели, где Customer — гигантский класс на 200 полей.
Карта контекстов (Context Mapping)
Описывает, как контексты взаимодействуют:
- Partnership — совместная эволюция (редко);
- Shared Kernel — общая подсистема с жёсткой синхронизацией (риск);
- Customer-Supplier — один контекст зависит от другого, но поставщик учитывает потребности;
- Conformist — потребитель принимает модель поставщика как есть (например, внешний API);
- Anticorruption Layer (ACL) — адаптер, защищающий внутреннюю модель от «токсичной» внешней.
Пример: интеграция с устаревшей системой — ACL преобразует её XML-выход в доменные события.
ACL — один из самых мощных инструментов архитектора. Он позволяет:
- сохранить чистоту внутренней модели;
- изолировать изменения в legacy-системе;
- постепенно заменять внешние зависимости.
Стратегическое проектирование
DDD предлагает начинать с:
- Глоссария (Ubiquitous Language) — совместное определение терминов с экспертами предметной области;
- Выявления агрегатов — корневых сущностей с транзакционными границами (например,
Order+OrderLines); - Разделения на Core Domain / Supporting Subdomains / Generic Subdomains — чтобы сосредоточить инновации там, где они дают конкурентное преимущество.
DDD особенно полезен при:
- высокой сложности бизнес-логики;
- наличии нескольких команд;
- долгосрочной поддержке системы (5+ лет).
18. Оптимизация кодовой базы: не ради скорости, а ради сопровождаемости
Архитектор часто сталкивается с требованием «ускорить систему». Но реальная оптимизация — это снижение когнитивной нагрузки при чтении и изменении кода.
Принципы читаемой архитектуры
-
Явность превыше краткости
Плохо:var r = svc.P(u, f)
Хорошо:var report = reportingService.GenerateProfitReport(user, filter) -
Отсутствие «магии»
Автоматическая инъекция зависимостей — хорошо; автоматическая генерация SQL по именам методов — плохо (непредсказуемо, трудно отлаживать). -
Единый стиль во всей базе
Единые правила:
— именование методов (Create,Update,Delete,GetByX);
— обработка ошибок (исключения vsResult<T>);
— логирование (структурное, с ключевыми полями). -
Документирование намерения, а не реализации
Комментарий// Using Dijkstra for shortest pathбесполезен — это видно из кода.
Комментарий// Must use Dijkstra (not A*) because edge weights can be negative— ценен.
Инструменты поддержки
- Архитектурные линтеры — проверяют соответствие слоёв, отсутствие циклических зависимостей.
- Автоматическая документация API — OpenAPI/Swagger, но с описаниями, а не только схемами.
- Кодовые соглашения в CI — запрет на
TODOбез issue, обязательные код-ревью для изменений в Core.
Оптимизированная кодовая база — та, в которую страшно вносить баги, потому что структура подсказывает правильное решение.
19. Проектирование для архитектурной целостности и масштабируемости
Завершающий синтез: как спроектировать систему, которая не «развалится» через три года?
Архитектурная целостность — это дисциплина
- Архитектурный совет (Architecture Review Board) — регулярные сессии (раз в 2–4 недели), где рассматриваются:
- новые границы;
- нарушения контрактов;
- технический долг с оценкой ROI ремонта.
- Архитектурные решения как документы (ADR) — фиксация почему было принято решение, какие альтернативы отклонены и почему. Хранятся в репозитории, версионируются.
- «Архитектурный долг» в бэклоге — явные задачи: «Выделить ACL для ERP-интеграции», «Перейти с REST на gRPC для внутренних вызовов».
Масштабируемость — это про измеримость
Масштабируемость не проверяется «на глаз». Она выражается в:
- SLI (Service Level Indicators) — latency, error rate, throughput;
- SLO (Service Level Objectives) — целевые значения (например, p99 latency < 200 мс при 1000 RPS);
- SLA (Service Level Agreements) — обязательства перед клиентами.
Архитектор определяет граничные условия:
Система должна поддерживать 10× рост числа пользователей без изменения архитектуры — только за счёт горизонтального масштабирования. При 100× росте допустима декомпозиция на новые микросервисы, но без переписывания ядра.
Стратегия эволюции
- Стратегия «Strangler Fig» — постепенное замещение монолита: новые фичи — в новых сервисах, старые — постепенно переносятся. Обратный прокси (например, на основе Envoy) маршрутизирует запросы.
- Флаги возможностей (feature flags) — позволяют включать/выключать логику без деплоя, проводить A/B-тесты архитектурных решений.
- Архитектурные «пробы» — выделение временного сервиса на 2–3 месяца для проверки гипотезы (например, «Event Sourcing сократит время отладки на 30 %»). По итогу — решение: масштабировать или свернуть.
20. Архитектура игровых приложений: Unity и Unreal Engine как среды проектирования
Разработка игр — это проектирование реактивных, stateful-систем с жёсткими временными ограничениями (обычно 16.6 мс на кадр при 60 FPS). Архитектурные решения здесь определяются стабильностью фреймрейта, предсказуемостью поведения и возможностью итеративного дизайна.
Unity и Unreal — это целостные среды, включающие редактор, сценографию, систему ассетов, сериализацию и runtime. Архитектура должна учитывать эту двойственность: дизайн-тайм vs runtime, редактор vs билд, data-driven vs code-driven.
20.1. Общие архитектурные вызовы в играх
- Жёсткие временные рамки. Любая операция (физика, рендер, AI) должна укладываться в бюджет кадра. Блокирующие вызовы, неожиданные GC-паузы, непредсказуемые аллокации — недопустимы.
- Состояние как центр. Игровой мир — это гигантский граф объектов с изменяющимися свойствами. Сохранение/загрузка, откат, сетевая репликация — всё строится на управлении этим состоянием.
- Итеративность дизайна. Геймдизайнеры меняют правила в процессе тестирования. Архитектура должна позволять вносить изменения без перекомпиляции (через скрипты, конфиги, инспектор).
- Смешанная ответственность. Один объект (например,
Player) часто объединяет физику, анимацию, UI, звук, сохранение — что в enterprise-системах было бы нарушением SRP. Компромисс здесь осознан: ради производительности и удобства работы в редакторе.
20.2. Unity: архитектурные стратегии
Unity поощряет компонентную модель (ECS — Entity Component System — опциональна, но рекомендуется для сложных проектов). Однако архитектор должен осознанно выбирать уровень абстракции.
Уровни проектирования в Unity:
-
MonoBehaviour-уровень (наивный подход)
Прямое наследованиеMonoBehaviour, логика вUpdate(),OnCollisionEnter()и т.п.
Риски:
— тесная связь с движком (нельзя unit-тестировать безPlayMode);
— трудности с рефакторингом (логика размазана поStart,Update,FixedUpdate);
— проблемы с жизненным циклом (активация/деактивация объектов вызывает неожиданные побочные эффекты). -
Сервисный уровень (Application Layer)
Вынос бизнес-логики в обычные C#-классы (GameStateManager,InventorySystem), которые инкапсулируют правила и не знают оGameObject.MonoBehaviourстановится адаптером между движком и доменом:public class PlayerController : MonoBehaviour {
private PlayerLogic _logic;
void Awake() => _logic = new PlayerLogic();
void Update() => _logic.Update(Input.GetAxis("Horizontal"));
void OnCollisionEnter(Collision c) => _logic.OnHit(c.relativeVelocity);
}Преимущества:
— тестируемость;
— возможность переиспользовать логику в серверной части (например, для authoritative server в мультиплеере);
— чёткое разделение «что» и «как». -
ECS / DOTS (Data-Oriented Tech Stack)
Для high-performance сценариев (тысячи сущностей, симуляции):- Entities — идентификаторы без поведения;
- Components — чистые данные (
struct); - Systems — обрабатывают группы компонентов (
JobComponentSystem).
Архитектурные последствия:
— отказ от OOP в пользу data-oriented design;
— сложность отладки (отсутствие привычных ссылок, объектов);
— необходимость явного управления жизненным циклом (EntityManager).
Ключевые архитектурные практики в Unity:
- Событийная система поверх UnityEvent или C# events — избегать прямых вызовов
GetComponent<Other>().DoSomething()между объектами. Вместо этого —event Action<Vector3> OnPositionChanged. - Инъекция зависимостей (Zenject, VContainer) — позволяет собирать иерархии объектов без
FindObjectOfType, поддерживать тестируемость. - Data-Oriented конфигурации — ScriptableObject как immutable-конфиги (
WeaponStats,LevelConfig). Они сериализуются в редакторе, проверяются статически, легко версионируются. - Feature Flags для баланса — выносить в настроечные ассеты, управляемые через Addressables.
20.3. Unreal Engine: архитектурные особенности
Unreal сочетает C++ и Blueprints, что создаёт уникальный контекст: архитектура — это договорённость между программистами и дизайнерами.
Основные уровни:
-
C++ Core Layer
Критически важная логика: сетевая репликация, сохранение, математика, low-level взаимодействие. Пишется на C++ с использованием UClass, UPROPERTY, RPC-макросов.
Принципы:
—UCLASS()— только для того, что должно сериализоваться или реплицироваться;
— бизнес-логику выносить в обычные классы (FGameRules), не наследующиеUObject;
— избегать «Blueprint-нативных» методов (UFUNCTION(BlueprintCallable)) без необходимости — они создают жёсткую связь. -
Blueprint Layer
Визуальное программирование для геймплея, UI, анимации. Архитектор должен определить:
— что может делаться в Blueprints, а что — только в C++;
— как обеспечить совместимость при изменении интерфейсов (например, черезVersionуUBlueprintи миграции). -
Data Assets (Data-Driven Design)
UDataAsset— аналог ScriptableObject в Unity. Примеры:UCharacterClassData,ULevelRewardTable.
Преимущества:
— геймдизайнеры меняют баланс без компиляции;
— единая точка истины для параметров;
— поддержка локализации через FText.
Архитектурные паттерны в Unreal:
-
Gameplay Ability System (GAS) — официальный фреймворк для сложных взаимодействий (умения, эффекты, стейты). Он вводит:
—Ability— конкретное действие (FireGun,Dash);
—AttributeSet— параметры (Health,Stamina);
—GameplayEffect— модификаторы (+10 Damage for 5 sec).
GAS — это архитектурный каркас, а не просто плагин. Его внедрение требует дисциплины, но даёт:
— предсказуемую репликацию;
— отладку через Gameplay Debugger;
— композицию эффектов без «спагетти-кода». -
Subsystems — глобальные сервисы (
UGameStateSubsystem,UAnalyticsSubsystem). Позволяют избежать синглтонов иGetWorld()->GetSubsystem<T>(). -
Plugin-Based Architecture — вынесение функционала в плагины (
OnlineSubsystem,ProceduralMeshPlugin). Это:
— изоляция зависимостей;
— возможность отключения функций под билд (Mobile vs PC);
— чистый core проекта.
Важное замечание по кроссплатформенности:
В играх архитектурные границы часто совпадают с платформенными модулями. Например:
Core— логика, не зависящая от платформы;Platform.Android/Platform.iOS— нативные вызовы;Input.Legacy/Input.Enhanced— разные системы ввода.
Такая структура позволяет легко добавлять поддержку новых ОС без пересборки всей игры.
21. Организация кодовой базы: группировка по файлам, папкам, зонам ответственности
Структура файловой системы — это видимое проявление архитектуры. Плохая организация — первый признак деградации: «а где у нас обработка заказов?», «почему этот файл в Utils, но он связан только с отчётами?».
Принцип: файловая структура должна отражать архитектурные границы, а не технические категории.
21.1. Анти-паттерны организации кода
-
По типам файлов
/Models
/Views
/Controllers
/Services
/DTOsПроблема: чтобы внести фичу «оплата», нужно прыгать между папками. Нет локальности изменений.
-
По технологиям
/Database
/API
/UI
/TestsСмешивает слои и функционал. Код «экспорта в PDF» может быть в
/Services,/Utils,/Jobs— неясно, где искать. -
По историческим причинам
/Legacy,/OldIntegration,/V2— технический долг, закреплённый в файловой системе.
21.2. Feature-Based (Domain-Driven) структура
Код группируется по бизнес-возможностям или ограниченным контекстам:
/src
/Core # Ядро: доменные модели, правила (без зависимостей от фреймворков)
/Entities
/ValueObjects
/DomainEvents
/Specifications
/Application # Сценарии использования: команды, запросы, обработчики
/Orders
CreateOrderCommand.cs
CreateOrderHandler.cs
OrderDto.cs
/Payments
ProcessPaymentCommand.cs
...
/Infrastructure # Реализация: БД, интеграции, внешние сервисы
/Persistence
/EF # Entity Framework
/Dapper
/ExternalServices
/ERPClient
/NotificationService
/Presentation # UI-слои (может быть несколько)
/WebApi
/Controllers
/Swagger
/BlazorApp
/MobileApp (отдельный проект)
/Shared # Межконтекстные примитивы (осторожно!)
/Kernel
Result.cs
EntityId.cs
/Common
DateTimeProvider.cs
Преимущества:
- Локальность изменений: фича «отмена заказа» — только в
/Orders; - Ясность границ:
Coreне может ссылаться наInfrastructure; - Поддержка модульности:
/Ordersможно вынести в отдельный проект/репозиторий.
21.3. Правила именования и видимости
- Интерфейсы и реализации — в одном файле или рядом:
IOrderRepository.cs,OrderRepository_EF.cs,OrderRepository_Mock.cs. - Внутренние классы —
internal, а неpublic; используютInternalsVisibleToтолько для тестов. - Файлы — по одному классу (кроме вложенных, DTO, enums) — упрощает поиск и merge.
21.4. Инструменты поддержки структуры
.editorconfig— единые правила отступов, кодировки, окончаний строк.Directory.Build.props(MSBuild) — централизованные настройки проектов (.NET).deps.json/project-graph— визуализация зависимостей между модулями (NDepend,dotnet list dependencies).- Pre-commit hooks — запрет коммитов с
TODO, непроверенных миграций, нарушений структуры.
21.5. Для игровых проектов — адаптированная структура
В Unity/Unreal файловая организация тесно связана с ассетами, поэтому используется гибрид:
/Assets
/Scripts
/Core
/Domain
PlayerState.cs
GameState.cs
/Systems
MovementSystem.cs
CombatSystem.cs
/Application
/Features
/Inventory
InventoryController.cs
ItemData.cs (ScriptableObject)
/Infrastructure
/Save
SaveService.cs
/Networking
PhotonAdapter.cs
/Plugins # Внешние SDK (не в /Scripts!)
/Resources # Только то, что требует Resources.Load (минимизировать)
/Addressables # Для динамической загрузки
В Unreal — через Content Folders и Plugins:
/Source
/MyGame.Core # C++ core
/MyGame.Gameplay # GAS, Abilities
/MyGame.UI # UMG, ViewModels
/Plugins
/Analytics
/LootboxSystem
Ключевой принцип: ассеты и код — единая система. Если WeaponData.asset используется только в CombatSystem, он должен находиться рядом (/Combat/WeaponData.asset), а не в /Data/Weapons.